/*
 * Copyright (c) 2011-2012 Alexander Dubu
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * o  Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * o  Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * o  Neither the name Axil nor the names of its contributors may be used to
 *    endorse or promote products derived from this software without specific
 *    prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package axil.stdlib.text.type;

import axil.api.*;
import axil.framework.Functions;
import axil.framework.localization.TranslationValue;
import axil.framework.type.Added;
import axil.framework.type.Subtracted;
import axil.stdlib.collection.type.Keyed;
import axil.stdlib.core.type.Nil;
import axil.stdlib.numbers.type.Int;

import java.io.IOException;
import java.io.Writer;
import java.util.Iterator;

import static axil.framework.Functions.*;


/**
 * An Axil representation of a string as an object.
 */
public class Str extends Text implements TranslationValue, Added, Subtracted,
	Keyed {
	public static final Str
	nul = new Str("(Null)"),
	blank = new Str("");
	public static final AxilType type = Str_Type.object;

	private final String value;


	public static Str from(String value) {
		if (value == null) {
			return nul;
		}
		return value.length() == 0 ? blank : new Str(value);
	}


	public Str(String value) {
		this.value = value;
	}


	/**
	 * Get the type metadata for this object. The type metadata contains
	 * information about the type itself, as an object.
	 */
	public AxilType type() {
		return type;
	}


	/**
	 * Returns a hash code value for the object. All Axil values must provide a
	 * meaningful implementation of this function to enable themselves to be
	 * placed in a map or set.
	 */
	protected int hash() {
		return value.hashCode();
	}


	/**
	 * Tell if this object is equivalent to the given object. The object given
	 * is never null. The object given may be of any type of value object.  If
	 * the given object is not a suitable type for comparison, a
	 * ClassCastException may be thrown.
	 */
	public boolean equalTo(AxilObject object) {
        // "nil" comparison; nil is not equal to anything.
        if (object == Nil.object) {
            return false;
        }
        // All others compare to value converted to a string.
        return value.equals(object.toString());
	}


	/**
	 * Compares this object with the specified object for order. If the given
	 * object is not a suitable type for comparison, a ClassCastException may
	 * be thrown.
	 *
	 * @param object
	 * 	The object to compare against. The given object cannot be null but may
	 * 	be any Axil object.
	 *
	 * @return
	 * 	Returns a negative integer, zero, or a positive integer as this object
	 * 	is less than, equal to, or greater than the specified object.
	 */
	public int comparedTo(AxilObject object) {
        // "nil" comparison; always return 1 (nil less than everything).
        if (object == Nil.object) {
            return 1;
        }
        // All others compare to value converted to a string.
        return value.compareTo(object.toString());
    }


	/**
	 * Return a subset of this object. The begin and end are zero-based indexes.
	 * The end may be -1, indicating the natural end of the list. The begin/end
	 * values are automatically adjusted to be within bounds.
	 */
	public Str slice(int begin, int end) {
		final int size = size();
		if (begin >= size) {
			return blank;
		}
		if ((end < 0) || (end >= size)) {
			end = size - 1;
		}
		return from(value.substring(begin, end + 1));
	}


	/**
	 * Return the key where the given object resides within this collection.
	 * A nil value is returned if the object does not live in this collection.
	 */
	public AxilObject keyof(AxilObject object) {
		if (object == Nil.object) {
			return object;
		}
		char ch = cast(Char.class, object).value();
		int i = value.indexOf(ch);
		return i < 0 ? Nil.object : Int.from(i + 1);
	}


	/**
	 * Return the object at the given key, or nil if the key is not contained
	 * within this object.
	 */
	public AxilObject at(AxilObject key) {
		int i = cast(Int.class, key).value() - 1;
		if ((i >= 0) && (i < size())) {
			return Char.from(value.charAt(i));
		}
		return Nil.object;
	}


	/**
	 * Perform string concatenation with this object. Regardless of the object,
	 * the value is converted into a string and appended to this object.
	 */
	public AxilObject add(AxilObject n) {
		String o = n.toString();
		if (Functions.empty(o)) {
			return this;
		}
		if (Functions.empty(value)) {
			return n;
		}
		return from(value + o);
	}


	/**
	 * Remove all of the occurrences of the given object from this object. The
	 * operand given must be of a Text type.
	 */
	public AxilObject subtract(AxilObject n) {
		if (! (n instanceof Text)) {
			throw error(axil(), "cannot-subtract",
			            nv("left", this.type()),
			            nv("right", n.type()));
		}
		final String t = n.toString();
		final int size = t.length();
		switch(size) {
		case 0:
			return this;

		case 1:
			final int max = value.length();
			final char discard = t.charAt(0);
			StringBuilder b = new StringBuilder(max);
			for (int i = 0; i < max; i++) {
				char ch = value.charAt(i);
				if (ch != discard) {
					b.append(ch);
				}
			}
			return from(b.toString());

		default:
			String s = value;
			while (true) {
				int i = s.indexOf(t);
				switch(i) {
					case -1:
						return from(s);

					case 0:
						if ((i + size) >= s.length()) {
							return blank;
						}
						s = s.substring(i + size);
						break;

					default:
						s = s.substring(0, i) + s.substring(i + size);
				}
			}
		}
	}


	/**
	 * Get the size of this text object. The size returned is always greater
	 * than or equal to zero.
	 */
	public int size() {
		return value.length();
	}


	private static final class Iter implements Iterator<Char> {
		private final String text;
		private final int size;
		private int index;


		private Iter(String text) {
			this.text = text;
			this.size = text.length();
			this.index = 0;
		}


		/**
		 * Returns true if the iteration has more elements. (In other words, returns
		 * true if next would return an element rather than throwing an exception.)
		 */
		public boolean hasNext() {
			return index < size;
		}


		/**
		 * Returns the next element in the iteration.
		 */
		public Char next() {
			Char ac = Char.from(text.charAt(index));
			index ++;
			return ac;
		}


		/**
		 *
		 * Removes from the underlying collection the last element returned by the
		 * iterator. This is an unsupported operation.
		 */
		public void remove() {
			throw new UnsupportedOperationException(
			    "Objects cannot be removed from this collection.");
		}

	}
	/**
	 * Returns an iterator over a set of characters.
	 */
	public Iterator<Char> iterator() {
		return new Iter(value);
	}


	/**
	 * Get the character at the given index (from zero). If the index is outside
	 * of the range of the object, then an exception is thrown.
	 */
	public char ch(int index) {
		return value.charAt(index);
	}


	/**
	 * Get the element at the given index, where the first object is at index
	 * zero. If there is no element at that position, then a null is returned.
	 */
	public Char at(int index) {
		return index < value.length() ? Char.from(value.charAt(index)) : null;
	}


	/**
	 * Get the first element from this sequential collection, or return null
	 * if this collection is empty.
	 */
	public Char first() {
		return at(0);
	}


	/**
	 * Get the last element from this sequential collection, or return null if
	 * this collection is empty.
	 */
	public Char last() {
		return empty() ? null : at(size() - 1);
	}


	/**
	 * Tell if this object, represents a true or false value. In general, true
	 * is returned for any object that is not equivalent to nil, is a number
	 * greater than zero, or is the boolean true value.
	 */
	public boolean bool() {
		return ! value.isEmpty();
	}


	/**
	 * Return the host application (or built-in Java) object that most closely
	 * matches this value object. Return returned value is never null unless
	 * this value object represents the special 'nil' object.
	 */
	public Object intrinsic() {
		return value;
	}



	/**
	 * Get the native Java value in its most natural form. This is usd by the
	 * low-level inner workings of Axil.
	 */
	public String value() {
		return value;
	}


	/**
	 * Return the text value appropriate for the given size. The size is used
	 * to select an appropriate translation when plurals are involved.
	 */
	public String text(int size) {
		return value;
	}


	/**
	 * Format this object in a locale-friendly manner. The string returned is
	 * a localized representation of this object using the locale and timezone
	 * associated with this persona. The formatting given is never null and
	 * matches formatting suitable for this type.
	 */
	public String format(Persona persona, Formatting formatting) {
		return value;
	}


	/**
	 * Return the translated, localized and formatted contents of this
	 * translation. The size is used to select an appropriate translation when
	 * plurals are involved.
	 */
	public String format(Persona persona, int size, Context values) {
		return value;
	}


	/**
	 * Write the translated, localized and formatted contents of this
	 * translation into the given output stream. The size is used to select an
	 * appropriate translation when plurals are involved.
	 */
	public void write(Persona persona, int size, Writer output,
	                  Context values) throws IOException {
		output.write(value);

	}


	/**
	 * Return a string representation of this object. The string returned is
	 * never null.
	 */
	public String toString() {
		return value;
	}
}
